from typing import Any, Dict, List, cast
from unittest.mock import AsyncMock, patch

import pytest

from api_utils.utils import (
    _extract_json_from_text,
    _get_latest_user_text,
    collect_and_validate_attachments,
    generate_sse_stop_chunk_with_usage,
    maybe_execute_tools,
    prepare_combined_prompt,
)
from models import FunctionCall, Message, MessageContentItem, ToolCall


@pytest.fixture
def mock_tools_registry():
    with (
        patch("api_utils.utils.register_runtime_tools") as mock_reg,
        patch("api_utils.utils.execute_tool_call", new_callable=AsyncMock) as mock_exec,
    ):
        yield mock_reg, mock_exec


@pytest.fixture
def mock_logger():
    with patch("logging.getLogger") as mock:
        yield mock.return_value


@pytest.fixture
def mock_file_utils():
    with (
        patch("api_utils.utils.extract_data_url_to_local") as mock_extract,
        patch("api_utils.utils.save_blob_to_local") as mock_save,
        patch("api_utils.utils.os.path.exists") as mock_exists,
    ):
        mock_exists.return_value = True
        yield mock_extract, mock_save, mock_exists


def test_prepare_combined_prompt_basic(mock_logger):
    """Test basic text message formatting."""
    messages = [
        Message(role="user", content="Hello"),
        Message(role="assistant", content="Hi there"),
    ]
    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "用户:\nHello" in prompt
    assert "助手:\nHi there" in prompt
    assert len(files) == 0


def test_prepare_combined_prompt_system(mock_logger):
    """Test system prompt handling."""
    messages = [
        Message(role="system", content="Be helpful"),
        Message(role="user", content="Hi"),
    ]
    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "系统指令:\nBe helpful" in prompt
    assert "用户:\nHi" in prompt
    # System message should not be repeated in conversation history if processed
    assert prompt.count("Be helpful") == 1


def test_prepare_combined_prompt_tools(mock_logger):
    """Test tool definitions injection."""
    tools = [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get weather",
                "parameters": {"type": "object"},
            },
        }
    ]
    messages = [Message(role="user", content="Weather?")]

    prompt, files = prepare_combined_prompt(
        messages, "req1", tools=tools, tool_choice="auto"
    )

    assert "可用工具目录:" in prompt
    assert "- 函数: get_weather" in prompt
    assert "参数模式:" in prompt


def test_prepare_combined_prompt_multimodal_image(mock_file_utils, mock_logger):
    """Test image url processing."""
    mock_extract, _, _ = mock_file_utils
    mock_extract.return_value = "/tmp/image.png"

    # Use dicts for Pydantic parsing
    content_item = {
        "type": "image_url",
        "image_url": {"url": "data:image/png;base64,...", "detail": "high"},
    }

    messages = [
        Message(role="user", content=cast(List[MessageContentItem], [content_item]))
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/image.png" in files
    assert "[图像细节: detail=high]" in prompt


def test_prepare_combined_prompt_multimodal_dict(mock_file_utils, mock_logger):
    """Test dictionary content processing."""
    mock_extract, _, _ = mock_file_utils
    mock_extract.return_value = "/tmp/image.png"

    content = [
        {"type": "text", "text": "Look at this"},
        {"type": "image_url", "image_url": {"url": "data:image...", "detail": "low"}},
    ]

    messages = [Message(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "Look at this" in prompt
    assert "/tmp/image.png" in files
    assert "[图像细节: detail=low]" in prompt


def test_prepare_combined_prompt_audio(mock_file_utils, mock_logger):
    """Test audio input processing."""
    _, mock_save, _ = mock_file_utils
    mock_save.return_value = "/tmp/audio.mp3"

    content_item = {
        "type": "input_audio",
        "input_audio": {
            "data": "SGVsbG8=",  # Valid base64 "Hello"
            "mime_type": "audio/mp3",
        },
    }

    messages = [
        Message(role="user", content=cast(List[MessageContentItem], [content_item]))
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/audio.mp3" in files


def test_prepare_combined_prompt_tool_calls(mock_logger):
    """Test tool call visualization."""
    tool_call = ToolCall(
        id="call1",
        type="function",
        function=FunctionCall(name="get_weather", arguments='{"city": "Paris"}'),
    )

    messages = [Message(role="assistant", content=None, tool_calls=[tool_call])]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "请求调用函数: get_weather" in prompt
    assert '"city": "Paris"' in prompt


def test_prepare_combined_prompt_tool_results(mock_logger):
    """Test tool result inclusion."""
    messages = [Message(role="tool", content="Sunny", tool_call_id="call1")]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    assert "工具结果 (tool_call_id=call1):" in prompt
    assert "Sunny" in prompt


def test_extract_json_from_text():
    """Test JSON extraction from text."""
    text = 'Here is some json: {"key": "value"} end.'
    result = _extract_json_from_text(text)
    assert result == '{"key": "value"}'

    text_no_json = "Just text"
    assert _extract_json_from_text(text_no_json) is None

    text_invalid = "Bad json { key: value }"
    assert _extract_json_from_text(text_invalid) is None


def test_get_latest_user_text():
    """Test extracting latest user text."""
    messages = [
        Message(role="user", content="Hello"),
        Message(role="assistant", content="Hi"),
        Message(role="user", content="World"),
    ]
    assert _get_latest_user_text(messages) == "World"

    # Test with list content
    content = [{"type": "text", "text": "Part1"}, {"type": "text", "text": "Part2"}]
    messages = [Message(role="user", content=cast(List[MessageContentItem], content))]
    assert _get_latest_user_text(messages) == "Part1\nPart2"


@pytest.mark.asyncio
async def test_maybe_execute_tools(mock_tools_registry, mock_logger):
    """Test explicit tool execution logic."""
    _, mock_exec = mock_tools_registry
    mock_exec.return_value = "success"

    tools = [{"type": "function", "function": {"name": "test_func"}}]
    tool_choice = {"type": "function", "function": {"name": "test_func"}}

    messages = [Message(role="user", content='Call test_func with {"arg": 1}')]

    results = await maybe_execute_tools(messages, tools, tool_choice)

    assert results is not None
    assert len(results) == 1
    assert results[0]["name"] == "test_func"
    assert results[0]["result"] == "success"

    # Verify arguments extraction (simple JSON in text)
    # The current implementation of maybe_execute_tools tries to find JSON in user text
    mock_exec.assert_called_once()
    call_args = mock_exec.call_args[0]  # (name, args, tool_def)
    assert call_args[0] == "test_func"
    assert call_args[1] == '{"arg": 1}'  # It passes the extracted JSON string


def test_collect_and_validate_attachments(mock_file_utils, mock_logger):
    """Test attachment collection and validation."""
    mock_extract, _, mock_exists = mock_file_utils
    mock_exists.return_value = True
    mock_extract.return_value = "/tmp/data.png"

    # Mock request object
    class MockMessage:
        def __init__(self, role, content, attachments=None):
            self.role = role
            self.content = content
            self.attachments = attachments or []

    class MockRequest:
        attachments = ["/tmp/existing.png", {"url": "", "format": "mp3"},
    }

    messages = [
        Message(role="user", content=cast(List[MessageContentItem], [content_item]))
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/audio_data.mp3" in files


def test_prepare_combined_prompt_input_video_processing(mock_file_utils, mock_logger):
    """Test input video processing with various formats."""
    mock_extract, mock_save, mock_exists = mock_file_utils
    mock_exists.return_value = True

    # 1. Video URL (data:)
    mock_extract.return_value = "/tmp/video1.mp4"
    item1 = {
        "type": "input_video",
        "input_video": {"url": "data:video/mp4;base64,AAAA"},
    }

    # 2. Video raw base64 data
    mock_save.return_value = "/tmp/video2.mp4"
    item2 = {
        "type": "input_video",
        "input_video": {
            "data": "BBBB",  # Raw base64
            "mime_type": "video/mp4",
            "format": "mp4",
        },
    }

    # 3. Local file URL
    item3 = {"type": "input_video", "input_video": {"url": "file:///c:/video3.mp4"}}

    messages = [
        Message(
            role="user", content=cast(List[MessageContentItem], [item1, item2, item3])
        )
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/video1.mp4" in files
    assert "/tmp/video2.mp4" in files
    # file:///c:/video3.mp4 -> /c:/video3.mp4 (unquoted)
    # Windows path handling in test environment might vary, checking endswith
    assert any(f.endswith("video3.mp4") for f in files)

    # Check that extract and save were called
    mock_extract.assert_called()
    mock_save.assert_called()


def test_prepare_combined_prompt_complex_nested_dict(mock_file_utils, mock_logger):
    """Test nested dictionary content with specific attachment keys."""
    mock_extract, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    content = {
        "text": "Look at these files",
        "images": [{"url": "file:///c:/img1.png"}],
        "files": [{"path": "c:/doc1.pdf"}],  # absolute path
        "media": [{"url": "data:video..."}],  # data url
    }

    mock_extract.return_value = "/tmp/media.mp4"

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "Look at these files" in prompt
    assert "/tmp/media.mp4" in files
    assert any(f.endswith("img1.png") for f in files)
    assert "c:/doc1.pdf" in files or "/c:/doc1.pdf" in files or "c:\\doc1.pdf" in files


@pytest.mark.asyncio
async def test_maybe_execute_tools_choice_logic(mock_tools_registry, mock_logger):
    """Test tool_choice logic (none, auto, specific)."""
    _, mock_exec = mock_tools_registry

    tools = [{"type": "function", "function": {"name": "func1"}}]
    messages = [Message(role="user", content="call func1")]

    # 1. Choice = 'none' -> returns None
    res = await maybe_execute_tools(messages, tools, "none")
    assert res is None

    # 2. Choice = 'auto' with 1 tool -> executes
    messages_with_json = [Message(role="user", content='call func1 {"arg": 1}')]

    mock_exec.return_value = "res1"
    res = await maybe_execute_tools(messages_with_json, tools, "auto")
    assert res is not None
    assert res[0]["name"] == "func1"

    # 3. Choice = specific name
    res = await maybe_execute_tools(messages_with_json, tools, "func1")
    assert res is not None
    assert res[0]["name"] == "func1"

    # 4. Choice = dict
    res = await maybe_execute_tools(
        messages_with_json, tools, {"type": "function", "function": {"name": "func1"}}
    )
    assert res is not None
    assert res[0]["name"] == "func1"


def test_collect_and_validate_attachments_error_handling(mock_file_utils, mock_logger):
    """Test error handling in attachment collection."""

    # Force an error during property access or iteration
    class BrokenMessage:
        @property
        def attachments(self):
            raise ValueError("Broken")

    class MockRequest:
        messages = [BrokenMessage()]

    initial_list = []
    # Should not raise exception, just log warning
    result = collect_and_validate_attachments(MockRequest(), "req1", initial_list)
    assert result == []


def test_prepare_combined_prompt_tool_result_list_content(mock_logger):
    """Test tool result with list content."""
    content = [{"type": "text", "text": "Result Part 1"}, "Result Part 2"]
    # Use model_construct to bypass strict Pydantic validation for mixed content types
    messages = [
        Message.model_construct(role="tool", content=content, tool_call_id="call1")
    ]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    assert "Result Part 1" in prompt
    assert "Result Part 2" in prompt


def test_prepare_combined_prompt_malformed_tool_args(mock_logger):
    """Test malformed JSON in tool call arguments."""
    tool_call = ToolCall(
        id="call1",
        type="function",
        function=FunctionCall(name="func1", arguments="{bad_json"),
    )
    messages = [Message(role="assistant", content=None, tool_calls=[tool_call])]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    # Should fall back to raw string
    assert "{bad_json" in prompt


def test_prepare_combined_prompt_unknown_content_type(mock_logger):
    """Test unknown content type warning."""

    class UnknownType:
        def __str__(self):
            return "UnknownObj"

    # Bypass Pydantic validation
    messages = [Message.model_construct(role="user", content=UnknownType())]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    assert "UnknownObj" in prompt


def test_prepare_combined_prompt_invalid_base64(mock_file_utils, mock_logger):
    """Test invalid base64 in audio/video input."""
    _, mock_save, _ = mock_file_utils
    mock_save.return_value = None

    item = {
        "type": "input_audio",
        "input_audio": {"data": "InvalidBase64!!!", "mime_type": "audio/mp3"},
    }

    messages = [Message(role="user", content=cast(List[MessageContentItem], [item]))]

    # Should not crash, just ignore or log error
    prompt, files = prepare_combined_prompt(messages, "req1")

    assert len(files) == 0


@pytest.mark.asyncio
async def test_maybe_execute_tools_existing_tool_message(
    mock_tools_registry, mock_logger
):
    """Test maybe_execute_tools returns None if tool result already exists."""
    messages = [
        Message(role="user", content="call func"),
        Message(
            role="assistant",
            content=None,
            tool_calls=[
                ToolCall(
                    id="1",
                    type="function",
                    function=FunctionCall(name="f", arguments="{}"),
                )
            ],
        ),
        Message(role="tool", content="result", tool_call_id="1"),
    ]

    res = await maybe_execute_tools(messages, [], "auto")
    assert res is None


@pytest.mark.asyncio
async def test_maybe_execute_tools_no_choice(mock_tools_registry, mock_logger):
    """Test maybe_execute_tools returns None if tool_choice is None."""
    messages = [Message(role="user", content="call func")]
    res = await maybe_execute_tools(messages, [], None)
    assert res is None


@pytest.mark.asyncio
async def test_maybe_execute_tools_execution_error(mock_tools_registry, mock_logger):
    """Test maybe_execute_tools handles execution errors gracefully."""
    _, mock_exec = mock_tools_registry
    mock_exec.side_effect = Exception("Execution failed")

    tools = [{"type": "function", "function": {"name": "func1"}}]
    messages = [Message(role="user", content='call func1 {"arg": 1}')]

    res = await maybe_execute_tools(messages, tools, "func1")
    assert res is None


@pytest.mark.asyncio
async def test_maybe_execute_tools_fallback_args(mock_tools_registry, mock_logger):
    """Test maybe_execute_tools uses empty dict when no JSON found."""
    _, mock_exec = mock_tools_registry
    mock_exec.return_value = "res"

    tools = [{"type": "function", "function": {"name": "func1"}}]
    # No JSON in content
    messages = [Message(role="user", content="call func1")]

    res = await maybe_execute_tools(messages, tools, "func1")

    assert res is not None
    assert res[0]["arguments"] == "{}"
    mock_exec.assert_called_with("func1", "{}")


def test_prepare_combined_prompt_tool_choice_string(mock_logger):
    """Test tool choice injection with specific string name."""
    tools = [{"type": "function", "function": {"name": "my_tool"}}]
    messages = [Message(role="user", content="hi")]

    # tool_choice as string name
    prompt, _ = prepare_combined_prompt(
        messages, "req1", tools=tools, tool_choice="my_tool"
    )

    assert "建议优先使用函数: my_tool" in prompt


def test_prepare_combined_prompt_tool_choice_dict(mock_logger):
    """Test tool choice injection with dictionary."""
    tools = [{"type": "function", "function": {"name": "my_tool"}}]
    messages = [Message(role="user", content="hi")]

    # tool_choice as dict
    tool_choice = {"type": "function", "function": {"name": "my_tool"}}
    prompt, _ = prepare_combined_prompt(
        messages, "req1", tools=tools, tool_choice=tool_choice
    )

    assert "建议优先使用函数: my_tool" in prompt


def test_prepare_combined_prompt_tools_error(mock_logger):
    """Test error handling during tools processing."""

    class BadTool:
        def get(self, k):
            raise ValueError("Bad tool")

    tools = [BadTool()]
    messages = [Message(role="user", content="hi")]

    # Should not crash
    prompt, _ = prepare_combined_prompt(
        messages, "req1", tools=cast(List[Dict[str, Any]], tools)
    )
    assert "用户:\nhi" in prompt


def test_prepare_combined_prompt_empty_content(mock_logger):
    """Test message with None content."""
    messages = [Message.model_construct(role="user", content=None)]
    prompt, _ = prepare_combined_prompt(messages, "req1")

    # If it's the only message and content is None/empty, it is skipped.
    assert prompt == ""


def test_prepare_combined_prompt_tool_result_list_exception(mock_logger):
    """Test tool result list processing exception handling."""

    class BadItem:
        def __str__(self):
            raise ValueError("Cannot stringify")

    messages = [
        Message.model_construct(role="tool", content=[BadItem()], tool_call_id="1")
    ]

    prompt, _ = prepare_combined_prompt(messages, "req1")
    # It should fall back to str(msg.content) in the except block
    assert "tool_call_id=1" in prompt


def test_collect_and_validate_attachments_top_level(mock_file_utils, mock_logger):
    """Test top level attachments in request."""
    mock_extract, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    class MockRequest:
        attachments = ["/tmp/top1.png", {"url": "file:///c:/top2.png"}]
        messages = []

    req = MockRequest()
    result = collect_and_validate_attachments(req, "req1", [])

    assert "/tmp/top1.png" in result
    assert any("top2.png" in f for f in result)


def test_collect_and_validate_attachments_initial_filter(mock_file_utils, mock_logger):
    """Test filtering of initial image list."""
    _, _, mock_exists = mock_file_utils

    # mock_exists side effect to filter
    def side_effect(path):
        return path == "/exists.png"

    mock_exists.side_effect = side_effect

    initial = ["/exists.png", "/missing.png", "relative.png"]

    # Need request object
    class MockRequest:
        messages = []

    result = collect_and_validate_attachments(MockRequest(), "req1", initial)

    assert "/exists.png" in result
    assert "/missing.png" not in result
    assert "relative.png" not in result


def test_prepare_combined_prompt_tool_fallback_name(mock_logger):
    """Test tool definition fallback when function is not a dict."""
    # Case: function is string, name is in top level
    tools = [{"function": "not_a_dict", "name": "fallback_tool"}]
    messages = [Message(role="user", content="hi")]

    prompt, _ = prepare_combined_prompt(messages, "req1", tools=tools)

    assert "- 函数: fallback_tool" in prompt


def test_prepare_combined_prompt_tool_params_unserializable(mock_logger):
    """Test tool params that cannot be serialized."""

    class NoJson:
        def __str__(self):
            return "NoJson"

    # Use model_construct or just dict if tools is list of dicts
    tools = [
        {
            "type": "function",
            "function": {
                "name": "bad_params",
                "parameters": NoJson(),  # json.dumps will fail
            },
        }
    ]
    messages = [Message(role="user", content="hi")]

    prompt, _ = prepare_combined_prompt(messages, "req1", tools=tools)

    # Should contain function name but skip params
    assert "- 函数: bad_params" in prompt
    assert "参数模式:" not in prompt


def test_prepare_combined_prompt_empty_system_message(mock_logger):
    """Test empty system message handling."""
    messages = [
        Message(role="system", content=""),  # Empty content
        Message(role="user", content="Hi"),
    ]
    prompt, _ = prepare_combined_prompt(messages, "req1")

    assert "系统指令:" not in prompt
    assert "用户:\nHi" in prompt


def test_prepare_combined_prompt_item_type_exception(mock_logger):
    """Test exception when accessing item.type."""

    class BadItem:
        @property
        def type(self):
            raise ValueError("No type")

        def __str__(self):
            return "BadItemStr"

    messages = [Message.model_construct(role="user", content=[BadItem()])]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    # Should swallow exception and ignore the item
    assert "BadItemStr" not in prompt
    # Since content is effectively empty and it's the only message, it should be skipped entirely
    assert prompt == ""


def test_prepare_combined_prompt_object_file_url(mock_file_utils, mock_logger):
    """Test object-style content with file_url/media_url."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    class UrlObj:
        def __init__(self, url):
            self.url = url

    class ItemWithFileUrl:
        type = "file_url"
        file_url = UrlObj("file:///c:/file.txt")

    class ItemWithMediaUrl:
        type = "media_url"
        media_url = UrlObj("file:///c:/media.mp4")

    messages = [
        Message.model_construct(
            role="user", content=[ItemWithFileUrl(), ItemWithMediaUrl()]
        )
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    # Check if files were added
    assert any("file.txt" in f for f in files)
    assert any("media.mp4" in f for f in files)


def test_prepare_combined_prompt_non_existent_local_file(mock_file_utils, mock_logger):
    """Test non-existent local file URL."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = False  # Not exists

    item = {"type": "file_url", "file_url": {"url": "file:///c:/missing.txt"}}
    messages = [Message(role="user", content=cast(List[MessageContentItem], [item]))]

    prompt, files = prepare_combined_prompt(messages, "req1")

    # Should not be in files list
    assert len(files) == 0


def test_collect_and_validate_attachments_detailed(mock_file_utils, mock_logger):
    """Test detailed attachment collection logic including top-level and various message keys."""
    mock_extract, _, mock_exists = mock_file_utils

    # Mock file existence
    def side_effect(path):
        return "existing" in path or "valid" in path

    mock_exists.side_effect = side_effect

    mock_extract.side_effect = lambda url, **kwargs: f"/tmp/{url.split('/')[-1]}"

    # Mock request object with various attachment fields
    class MockRequest:
        attachments = [
            "c:/tmp/valid_top_level.png",
            "c:/tmp/missing.png",
            {"url": ""},
            {"url": "file:///c:/valid_file_url.txt"},
            "",  # Empty string
            None,  # None
            {"url": ""},  # Empty URL in dict
        ]
        messages = [
            Message.model_construct(
                role="user",
                content="msg1",
                images=["c:/tmp/valid_image.png"],
                files=[{"path": "c:/tmp/valid_file.pdf"}],
                media=["file:///c:/valid_media.mp4"],
            )
        ]

    req = MockRequest()
    initial_list = ["c:/tmp/valid_initial.png", "c:/tmp/missing_initial.png"]

    result = collect_and_validate_attachments(req, "req1", initial_list)

    # Check initial list filtering
    assert "c:/tmp/valid_initial.png" in result
    assert "c:/tmp/missing_initial.png" not in result

    # Check top-level attachments
    assert "c:/tmp/valid_top_level.png" in result
    assert "c:/tmp/missing.png" not in result
    # data: URL -> extracted (mock extract returns /tmp/...)
    assert mock_extract.called

    # Check message-level attachments
    assert "c:/tmp/valid_image.png" in result
    assert "c:/tmp/valid_file.pdf" in result
    # file:///c:/valid_media.mp4 -> unquoted path
    assert any("valid_media.mp4" in f for f in result)


def test_prepare_combined_prompt_tool_edge_cases(mock_logger):
    """Test edge cases for tool definitions and choice."""
    # Malformed tool definition (missing function)
    tools = [
        {"type": "function"},  # Missing function dict
        {"name": "direct_name_tool"},  # Old style direct dict
    ]

    # Tool choice that doesn't match any tool
    tool_choice = "non_existent_tool"

    messages = [Message(role="user", content="Hi")]

    prompt, _ = prepare_combined_prompt(
        messages, "req1", tools=tools, tool_choice=tool_choice
    )

    # Should handle malformed tool gracefully (skip or partial log)
    # "direct_name_tool" might be processed if logic allows (it does: t.get('name'))
    assert "函数: direct_name_tool" in prompt

    # tool_choice "non_existent_tool" might still be suggested if logic just appends it
    assert "建议优先使用函数: non_existent_tool" in prompt


def test_prepare_combined_prompt_content_pydantic_objects(mock_file_utils, mock_logger):
    """Test content items as objects (simulating Pydantic models) instead of dicts."""
    mock_extract, mock_save, _ = mock_file_utils
    mock_save.return_value = "/tmp/saved.mp3"

    class InputAudio:
        data = "SGVsbG8="  # Valid base64 "Hello"
        mime_type = "audio/mp3"
        format = "mp3"

    class ContentItem:
        type = "input_audio"
        input_audio = InputAudio()

    messages = [Message.model_construct(role="user", content=[ContentItem()])]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/saved.mp3" in files


def test_prepare_combined_prompt_empty_url_strings(mock_logger):
    """Test content items with empty URL strings."""
    content = [
        {"type": "image_url", "image_url": {"url": ""}},
        {"type": "image_url", "image_url": {"url": "   "}},
        {"type": "text", "text": "Valid Text"},
    ]

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "Valid Text" in prompt
    assert len(files) == 0


def test_prepare_combined_prompt_fallback_str_content(mock_logger):
    """Test fallback when content is not string/list/dict (e.g. None or unexpected type)."""
    # None content handled in loop start usually, but let's try weird type
    messages = [Message.model_construct(role="user", content=12345)]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    # Should convert to string
    assert "12345" in prompt


def test_prepare_combined_prompt_input_image_with_detail(mock_file_utils, mock_logger):
    """Test input_image object with detail field (lines 234-244)."""
    mock_extract, _, _ = mock_file_utils
    mock_extract.return_value = "/tmp/input_image.png"

    class InputImage:
        url = ""
        detail = "high"

    class ContentItem:
        type = "image_url"

        def __init__(self):
            self.input_image = InputImage()

    messages = [Message.model_construct(role="user", content=[ContentItem()])]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/input_image.png" in files
    assert "[图像细节: detail=high]" in prompt


def test_prepare_combined_prompt_dict_image_url_with_detail(
    mock_file_utils, mock_logger
):
    """Test dict image_url with detail field (lines 266-268)."""
    mock_extract, _, _ = mock_file_utils
    mock_extract.return_value = "/tmp/image_detail.png"

    # Dict structure with nested image_url dict containing detail
    content = [
        {
            "type": "image_url",
            "image_url": {"url": "", "detail": "auto"},
        }
    ]

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/image_detail.png" in files
    assert "[图像细节: detail=auto]" in prompt


def test_prepare_combined_prompt_audio_absolute_path(mock_file_utils, mock_logger):
    """Test audio with absolute path (lines 427-431)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    # Absolute path for audio
    content_item = {
        "type": "input_audio",
        "input_audio": {"url": "c:/audio/test.mp3"},
    }

    messages = [
        Message(role="user", content=cast(List[MessageContentItem], [content_item]))
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "c:/audio/test.mp3" in files


def test_prepare_combined_prompt_video_absolute_path(mock_file_utils, mock_logger):
    """Test video with absolute path (lines 427-431)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    # Absolute path for video
    content_item = {
        "type": "input_video",
        "input_video": {"url": "/home/user/video.mp4"},
    }

    messages = [
        Message(role="user", content=cast(List[MessageContentItem], [content_item]))
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/home/user/video.mp4" in files


def test_get_latest_user_text_dict_non_text_type(mock_logger):
    """Test _get_latest_user_text with dict items that are not text type (lines 666-671)."""
    messages = [
        Message.model_construct(
            role="user",
            content=[
                {"type": "image", "url": "image.png"},  # Not text type
                {"type": "audio", "data": "..."},  # Not text type
                {"type": "text", "text": "Valid text"},
            ],
        )
    ]

    result = _get_latest_user_text(messages)

    # Should only extract text items
    assert result == "Valid text"


def test_get_latest_user_text_dict_content_empty(mock_logger):
    """Test _get_latest_user_text with dict content that has no text (lines 680-681)."""
    # Dict content with no text field
    messages = [
        Message.model_construct(role="user", content={"attachments": ["file.pdf"]})
    ]

    result = _get_latest_user_text(messages)

    assert result == ""


def test_get_latest_user_text_no_user_message(mock_logger):
    """Test _get_latest_user_text with no user messages (lines 680-681)."""
    messages = [
        Message(role="assistant", content="Hello"),
        Message(role="system", content="System"),
    ]

    result = _get_latest_user_text(messages)

    assert result == ""


@pytest.mark.asyncio
async def test_maybe_execute_tools_auto_single_tool_name_fallback(
    mock_tools_registry, mock_logger
):
    """Test maybe_execute_tools with auto choice, single tool, name at top level (lines 726-728)."""
    _, mock_exec = mock_tools_registry
    mock_exec.return_value = "success"

    # Tool with name at top level, no function dict
    tools = [{"name": "top_level_func"}]
    tool_choice = "auto"

    messages = [Message(role="user", content='call it {"arg": 1}')]

    results = await maybe_execute_tools(messages, tools, tool_choice)

    assert results is not None
    assert results[0]["name"] == "top_level_func"


@pytest.mark.asyncio
async def test_maybe_execute_tools_required_single_tool(
    mock_tools_registry, mock_logger
):
    """Test maybe_execute_tools with 'required' choice (lines 717-728)."""
    _, mock_exec = mock_tools_registry
    mock_exec.return_value = "result"

    tools = [{"type": "function", "function": {"name": "required_func"}}]
    tool_choice = "required"

    messages = [Message(role="user", content='{"x": 1}')]

    results = await maybe_execute_tools(messages, tools, tool_choice)

    assert results is not None
    assert results[0]["name"] == "required_func"


def test_prepare_combined_prompt_tool_params_json_dumps_error(mock_logger):
    """Test tool params that raise exception during json.dumps (lines 96-97)."""

    class UnserializableParams:
        """Object that cannot be JSON serialized."""

        def __iter__(self):
            raise TypeError("Cannot iterate")

    # Create tool with unserializable parameters
    tools = [
        {
            "type": "function",
            "function": {"name": "bad_tool", "parameters": UnserializableParams()},
        }
    ]

    messages = [Message(role="user", content="hi")]

    # Should not crash, just skip params serialization
    prompt, _ = prepare_combined_prompt(messages, "req1", tools=tools)

    # Tool name should still be present
    assert "- 函数: bad_tool" in prompt
    # But params should be skipped due to exception
    # The code has a try/except that passes on json.dumps failure


def test_collect_and_validate_attachments_empty_url_handling(
    mock_file_utils, mock_logger
):
    """Test attachment collection with empty URL strings (lines 802, 831, 834)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    class MockRequest:
        attachments = [
            "",  # Empty string
            "   ",  # Whitespace only
            {"url": ""},  # Empty URL in dict
            {"url": "   "},  # Whitespace URL in dict
            "c:/valid.png",  # Valid path
        ]
        messages = [
            Message.model_construct(
                role="user",
                content="test",
                images=["", "c:/valid_image.png"],
                files=[{"path": ""}, {"path": "c:/valid_file.pdf"}],
            )
        ]

    req = MockRequest()
    result = collect_and_validate_attachments(req, "req1", [])

    # Only valid paths should be included
    assert "c:/valid.png" in result
    assert "c:/valid_image.png" in result
    assert "c:/valid_file.pdf" in result

    # Empty strings should be filtered out
    assert "" not in result
    assert "   " not in result


def test_prepare_combined_prompt_dict_input_image_with_detail(
    mock_file_utils, mock_logger
):
    """Test dict input_image structure with detail field (lines 271-282)."""
    mock_extract, _, _ = mock_file_utils
    mock_extract.return_value = "/tmp/input_img_detail.png"

    content = [
        {
            "type": "image_url",
            "input_image": {
                "url": "",
                "detail": "low",
            },
        }
    ]

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/input_img_detail.png" in files
    assert "[图像细节: detail=low]" in prompt


def test_prepare_combined_prompt_dict_content_file_field(mock_file_utils, mock_logger):
    """Test dict content with generic 'file' field (lines 313-322)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    content = [
        {
            "type": "image_url",
            "file": {"url": "c:/from_file_field.png"},
        }
    ]

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "c:/from_file_field.png" in files


def test_prepare_combined_prompt_dict_content_file_path(mock_file_utils, mock_logger):
    """Test dict content with file.path field (lines 318-322)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    content = [
        {
            "type": "file_url",
            "file": {"path": "/absolute/path/file.pdf"},
        }
    ]

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/absolute/path/file.pdf" in files


def test_prepare_combined_prompt_object_url_attribute(mock_file_utils, mock_logger):
    """Test content item with direct url attribute (lines 249-250)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    class UrlItem:
        type = "image_url"
        url = "c:/direct_url.png"

    messages = [Message.model_construct(role="user", content=[UrlItem()])]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "c:/direct_url.png" in files


def test_prepare_combined_prompt_dict_attachments_nested_input_image(
    mock_file_utils, mock_logger
):
    """Test dict content attachments with nested input_image (lines 496-502)."""
    mock_extract, _, _ = mock_file_utils
    mock_extract.return_value = "/tmp/nested_input.png"

    content = {
        "text": "Check attachment",
        "attachments": [
            {"input_image": {"url": ""}},
        ],
    }

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "Check attachment" in prompt
    assert "/tmp/nested_input.png" in files


def test_prepare_combined_prompt_content_item_input_image_string(
    mock_file_utils, mock_logger
):
    """Test content item with input_image as string (lines 283-284)."""
    _, _, mock_exists = mock_file_utils
    mock_exists.return_value = True

    # This tests the case where item has input_image as a string directly
    content = [
        {
            "type": "image_url",
            "input_image": "c:/string_input_image.png",  # String, not dict
        }
    ]

    messages = [Message.model_construct(role="user", content=content)]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "c:/string_input_image.png" in files


def test_prepare_combined_prompt_audio_video_data_base64(mock_file_utils, mock_logger):
    """Test audio/video with raw base64 data not starting with 'data:' (lines 446-459)."""
    _, mock_save, _ = mock_file_utils
    mock_save.return_value = "/tmp/base64_audio.mp3"

    content_item = {
        "type": "input_audio",
        "input_audio": {
            "data": "SGVsbG8gV29ybGQ=",  # Not starting with "data:"
            "mime_type": "audio/mp3",
            "format": "mp3",
        },
    }

    messages = [
        Message(role="user", content=cast(List[MessageContentItem], [content_item]))
    ]

    prompt, files = prepare_combined_prompt(messages, "req1")

    assert "/tmp/base64_audio.mp3" in files
    mock_save.assert_called_once()


def test_prepare_combined_prompt_tool_result_no_tool_call_id(mock_logger):
    """Test tool result without tool_call_id (lines 579-586)."""
    messages = [Message.model_construct(role="tool", content="Result without ID")]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    # Should still include content, but no tool_call_id line
    assert "Result without ID" in prompt
    assert "tool_call_id=" not in prompt


def test_prepare_combined_prompt_assistant_empty_with_tool_calls(mock_logger):
    """Test assistant message with no content but has tool_calls (line 614)."""
    tool_call = ToolCall(
        id="call1",
        type="function",
        function=FunctionCall(name="func", arguments="{}"),
    )

    messages = [
        Message(role="assistant", content="", tool_calls=[tool_call])  # Empty content
    ]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    # Should include tool call visualization even with empty content
    assert "请求调用函数: func" in prompt


def test_prepare_combined_prompt_skip_empty_messages_edge_case(mock_logger):
    """Test edge case for empty message skipping (lines 616-621)."""
    # Edge case: First message with only role prefix, no content
    messages = [Message.model_construct(role="assistant", content="")]

    prompt, _ = prepare_combined_prompt(messages, "req1")

    # Should be empty since only role prefix and no content
    assert prompt == ""
